package org.mydba;

import oracle.jdbc.pool.OracleConnectionCacheManager;
import oracle.jdbc.pool.OracleConnectionPoolDataSource;
import oracle.jdbc.pool.OracleDataSource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.h2.jdbcx.JdbcConnectionPool;
import org.mydba.info.*;
import org.mydba.util.DBAHelper;

import javax.sql.*;
import java.io.File;
import java.io.IOException;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.WeakHashMap;
import java.util.concurrent.*;

/**
 * MyDBA aspect, it is the main entry point of MyDBA.
 *
 * @see <a href="http://www.eclipse.org/aspectj/doc/released/adk15notebook/ataspectj-pcadvice.html">Pointcuts and Advice</a>
 * @see <a href="http://andrewclement.blogspot.com/2009/02/load-time-weaving-basics.html">AspectJ Programming</a>
 * @since 1.0
 */
@Aspect
public class MyDBA {

    public static final String EXECUTIONS = "EXECUTIONS";
    public static final String EXEC_ID = "LOCAL_REPORT_DATABASE_EXEC_ID";
    public static final String RESULT_SET_STATISTIC = "RESULT_SET_STATISTIC";

    /**
     * Thread contexts.
     */
    static Map<Thread, Map<String, Object>> contexts = new WeakHashMap<Thread, Map<String, Object>>();

    /**
     * Login of my_dba account.
     */
    final static String myDBALogin;

    /**
     * Password of my_dba account.
     */
    final static String myDBAPassword;

    static {
        myDBALogin = System.getProperty("mydba.login.username");
        myDBAPassword = System.getProperty("mydba.login.password");
    }

    /**
     * Denotes that the DBA connection configuration is ok.
     */
    private static volatile boolean connectionConfigOk;

    /**
     * Denotes that MyDBA has been initialized regarding Oracle parameters.
     */
    private static volatile boolean oracleInitializationOk;

    /**
     * Denotes the DBA connection pool datasource.
     */
    private static volatile OracleConnectionPoolDataSource myDBACPDS;

    /**
     * Denotes the DBA datasource.
     */
    private static volatile OracleDataSource myDBADS;

    /**
     * Denotes the MyDBA reporting database connection pool.
     */
    private static volatile JdbcConnectionPool myDBAReportDatabaseConnectionPool;

    /**
     * Asynchronous call executor.
     */
    private static volatile ThreadPoolExecutor executor;

    private static volatile ArrayBlockingQueue executorQueue = new ArrayBlockingQueue(1000, true);

    static {
        try {
            myDBACPDS = new OracleConnectionPoolDataSource();
            myDBADS = new OracleDataSource();
        }
        catch (SQLException sqle) {
            sqle.printStackTrace();
        }
    }

    /**
     * Private constructor, no direct instanciation!
     */
    private MyDBA() {
        executor = new ThreadPoolExecutor(5, 50, 100, TimeUnit.MILLISECONDS, executorQueue);
        initializeReportDatabase();
    }

    /**
     * Initialize H2 in local file database.
     */
    private void initializeReportDatabase() {
        Connection reportCon = null;

        try {
            Class.forName("org.h2.Driver");

            final File dbDirectory = new File(".", "mydbadb");

            if (!dbDirectory.exists()) {
                final boolean creationOk = dbDirectory.mkdirs();

                if (!creationOk) {
                    throw new IllegalStateException("Can't create directory " + dbDirectory.getAbsolutePath() + " for MyDBA report database!");
                }
            }

            myDBAReportDatabaseConnectionPool = JdbcConnectionPool.create("jdbc:h2:file:mydbadb/reportdb;DB_CLOSE_ON_EXIT=FALSE", "sa", "");
            myDBAReportDatabaseConnectionPool.setMaxConnections(50);

            reportCon = getReportDbConnection();
            try {
                System.out.println("MyDBA database created: " + new File(dbDirectory, "reportdb").getCanonicalPath());
            }
            catch (IOException ioe) {
                System.out.println("MyDBA database created: " + new File(dbDirectory, "reportdb").getAbsolutePath());
            }

            LocalDBInitializer.initializeReportDb(getReportDbConnection());

            // Shutdown hook to note the date at the end of the run
            Runtime.getRuntime().addShutdownHook(
                    new Thread() {
                        public void run() {
                            while(!executorQueue.isEmpty()) {
                                System.out.println("Waiting 500ms for MyDBA asynchronous tasks to finish...");
                                try {
                                    Thread.sleep(500L);
                                }
                                catch( InterruptedException ignored ) {
                                }
                            }

                            DBAHelper.close(myDBADS);
                            DBAHelper.close(myDBACPDS);

                            Thread.currentThread().setName("MyDBA end time notifier");
                            Connection c = null;
                            PreparedStatement p = null;
                            try {
                                c = getReportDbConnection();
                                p = c.prepareStatement("update mydba_run set end_time=systimestamp, end_time_ns=?");
                                p.setLong(1, System.nanoTime());
                                p.executeUpdate();
                                c.commit();
                            }
                            catch (SQLException sqle) {
                                sqle.printStackTrace();
                            }
                            finally {
                                DBAHelper.close(p);
                                DBAHelper.close(c);
                            }

                            DBAHelper.close(myDBAReportDatabaseConnectionPool);
                        }
                    });
        }
        catch (ClassNotFoundException cnfe) {
            cnfe.printStackTrace();
        }
        catch (SQLException sqle) {
            sqle.printStackTrace();
        }
        finally {
            DBAHelper.close(reportCon);
        }
    }

    /**
     * Return a connection to the local report database.
     *
     * @return a connection to the local report database
     * @throws SQLException in case of problem
     */
    public static Connection getReportDbConnection() throws SQLException {
        final Connection c = myDBAReportDatabaseConnectionPool.getConnection();
        c.setAutoCommit(false);
        return c;
    }

    /**
     * Return a connection to the monitored database.
     *
     * @return a connection to the monitored database
     * @throws SQLException in case of problem
     */
    public static Connection getConnection() throws SQLException {
        final Connection c = myDBADS.getConnection();
        // TODO: prepare connection only if needed (note SID and prepare only unprepared session)
        DBAHelper.prepareConnection(c);
        return c;
    }

    public static Future executeAsynchronously(final Callable c) {
        return executor.submit(c);
    }

    /**
     * Initialize the connection pooled data source, the data source and the connection cache manager  to the monitored database.
     *
     * @param url the URL JDBC connection for the monitored database
     * @throws SQLException in case of problem
     */
    private void initializeMyDBAConnectionPooledDataSource(String url) throws SQLException {
        myDBACPDS.setURL(url);
        myDBACPDS.setUser(myDBALogin);
        myDBACPDS.setPassword(myDBAPassword);

        myDBADS.setURL(url);
        myDBADS.setUser(myDBALogin);
        myDBADS.setPassword(myDBAPassword);

        myDBADS.setConnectionCachingEnabled(true);

        final Properties properties = new Properties();
        properties.setProperty("MinLimit", "5");
        properties.setProperty("MaxLimit", "50");
        properties.setProperty("InactivityTimeout", "2");
        myDBADS.setConnectionProperties(properties);

        final OracleConnectionCacheManager occm = OracleConnectionCacheManager.getConnectionCacheManagerInstance();
        occm.createCache("MyDBAConnectionCache", myDBADS, properties);

        // Use OracleConnectionPoolDataSource instance so the cache
        // will be populated with connections from an OracleConnectionPoolDataSource
        // instance
        occm.setConnectionPoolDataSource("MyDBAConnectionCache", myDBACPDS);

        final Connection myDBAConnection = getConnection();

        if (!oracleInitializationOk) {
            LocalDBInitializer.initializeReportDbFromOracle(myDBAConnection);
            oracleInitializationOk = true;
        }

        DBAHelper.close(myDBAConnection);
    }

    /**
     * Setup DBA connection to be used for DBA inspection.
     *
     * @param url      url used to connect to database
     * @param login    login used to connect to database
     * @param password password used to connect to database
     */
    @AfterReturning(pointcut = "call(java.sql.Connection java.sql.DriverManager.getConnection(String,String,String)) " +
            "&& !execution(java.sql.Connection java.sql.DriverManager.getConnection(String,String,String)) " +
            "&& args(url,login,password)")
    public void setupDBAConnection(final String url, final String login, final String password) {
        if (connectionConfigOk) {
            return;
        }

        synchronized (MyDBA.class) {
            if (connectionConfigOk) {
                return;
            }

            try {
                initializeMyDBAConnectionPooledDataSource(url);

                connectionConfigOk = true;
            }
            catch (SQLException sqle) {
                sqle.printStackTrace();
            }
        }
    }

    /**
     * Setup DBA connection to be used for DBA inspection.
     *
     * @param url        url used to connect to database
     * @param properties properties used to connect to database
     */
    @AfterReturning(pointcut = "call(java.sql.Connection java.sql.DriverManager.getConnection(String,java.util.Properties)) " +
            "&& !execution(java.sql.Connection java.sql.DriverManager.getConnection(String,java.util.Properties)) " +
            "&& args(url,properties)")
    public void setupDBAConnection(final String url, final Properties properties) {
        if (connectionConfigOk) {
            return;
        }

        synchronized (MyDBA.class) {
            if (connectionConfigOk) {
                return;
            }

            try {
                initializeMyDBAConnectionPooledDataSource(url);

                connectionConfigOk = true;
            }
            catch (SQLException sqle) {
                sqle.printStackTrace();
            }
        }
    }

    /**
     * Setup DBA connection to be used for DBA inspection.
     *
     * @param originalDataSource data source used to connect to database
     */
    @AfterReturning(pointcut = "call(java.sql.Connection javax.sql.DataSource.getConnection()) " +
            "&& !execution(java.sql.Connection javax.sql.DataSource.getConnection()) " +
            "&& target(originalDataSource)")
    public void setupDBAConnection(final DataSource originalDataSource) {
        if (connectionConfigOk) {
            return;
        }

        synchronized (MyDBA.class) {
            if (connectionConfigOk) {
                return;
            }

            try {
                final Connection c = originalDataSource.getConnection(myDBALogin, myDBAPassword);
                final String url = c.getMetaData().getURL();
                DBAHelper.close(c);
                initializeMyDBAConnectionPooledDataSource(url);

                connectionConfigOk = true;
            }
            catch (SQLException sqle) {
                sqle.printStackTrace();
            }
        }
    }

    /**
     * Setup DBA connection to be used for DBA inspection.
     *
     * @param originalDataSource data source used to connect to database
     * @param login              original login
     * @param password           original password
     */
    @AfterReturning(pointcut = "call(java.sql.Connection javax.sql.DataSource.getConnection(String,String)) " +
            "&& !execution(java.sql.Connection javax.sql.DataSource.getConnection(String,String)) " +
            "&& target(originalDataSource) " +
            "&& args(login,password)")
    public void setupDBAConnection(final DataSource originalDataSource, final String login, final String password) {
        if (connectionConfigOk) {
            return;
        }

        synchronized (MyDBA.class) {
            if (connectionConfigOk) {
                return;
            }

            try {
                final Connection c = originalDataSource.getConnection(myDBALogin, myDBAPassword);
                final String url = c.getMetaData().getURL();
                DBAHelper.close(c);
                initializeMyDBAConnectionPooledDataSource(url);

                connectionConfigOk = true;
            }
            catch (SQLException sqle) {
                sqle.printStackTrace();
            }
        }
    }

    /**
     * Setup DBA connection to be used for DBA inspection.
     *
     * @param originalPooledDataSource pooled data source used to connect to database
     */
    @AfterReturning(pointcut = "call(javax.sql.PooledConnection javax.sql.ConnectionPoolDataSource.getPooledConnection()) " +
            "&& !execution(javax.sql.PooledConnection javax.sql.ConnectionPoolDataSource.getPooledConnection()) " +
            "&& target(originalPooledDataSource)")
    public void setupDBAConnection(final ConnectionPoolDataSource originalPooledDataSource) {
        if (connectionConfigOk) {
            return;
        }

        synchronized (MyDBA.class) {
            if (connectionConfigOk) {
                return;
            }

            try {
                final PooledConnection pc = originalPooledDataSource.getPooledConnection(myDBALogin, myDBAPassword);
                final Connection c = pc.getConnection();
                final String url = c.getMetaData().getURL();
                DBAHelper.close(c);
                initializeMyDBAConnectionPooledDataSource(url);

                connectionConfigOk = true;
            }
            catch (SQLException sqle) {
                sqle.printStackTrace();
            }
        }
    }

    /**
     * Setup DBA connection to be used for DBA inspection.
     *
     * @param originalPooledDataSource pooled data source used to connect to database
     * @param login                    original login
     * @param password                 original password
     */
    @AfterReturning(pointcut = "call(javax.sql.PooledConnection javax.sql.ConnectionPoolDataSource.getPooledConnection(String,String)) " +
            "&& !execution(javax.sql.PooledConnection javax.sql.ConnectionPoolDataSource.getPooledConnection(String,String)) " +
            "&& target(originalPooledDataSource) " +
            "&& args(login,password)")
    public void setupDBAConnection(final ConnectionPoolDataSource originalPooledDataSource, final String login, final String password) {
        if (connectionConfigOk) {
            return;
        }

        synchronized (MyDBA.class) {
            if (connectionConfigOk) {
                return;
            }

            try {
                final PooledConnection pc = originalPooledDataSource.getPooledConnection(myDBALogin, myDBAPassword);
                final Connection c = pc.getConnection();
                final String url = c.getMetaData().getURL();
                DBAHelper.close(c);
                initializeMyDBAConnectionPooledDataSource(url);

                connectionConfigOk = true;
            }
            catch (SQLException sqle) {
                sqle.printStackTrace();
            }
        }
    }

    /**
     * Pointcut to match java.sql.ResultSet java.sql.Statement.executeQuery(String).
     *
     * @param originalStmt the original statement on which the executeQuery has been called
     * @param query        the parameter of the call
     */
    @Pointcut("call(java.sql.ResultSet java.sql.Statement.executeQuery(String)) " +
            "&& !execution(java.sql.ResultSet java.sql.Statement.executeQuery(String)) " +
            "&& target(originalStmt) " +
            "&& args(query)")
    static void statementExecuteQueryPointcut(final Statement originalStmt, final String query) {
    }

    /**
     * Around aspect that retrieves information regarding previous pointcut.
     *
     * @param thisJoinPoint join point for proceed processing
     * @param originalStmt  the original statement on which the executeQuery has been called
     * @param query         the parameter of the call
     * @return a ResultSet as java.sql.ResultSet java.sql.Statement.executeQuery(String)
     * @throws Throwable (in fact an SQLException)
     */
    @Around("statementExecuteQueryPointcut(originalStmt,query)")
    public ResultSet executeQuery(final ProceedingJoinPoint thisJoinPoint,
                                  final Statement originalStmt,
                                  final String query) throws Throwable {
        long mydbaSTime = System.nanoTime();
        long mydbaTime = 0;

        final int SID = SnapshotHelper.getSessionSID(originalStmt.getConnection());

        Connection c = null;

        try {
            c = getConnection();

            final Map<Integer, OracleStatistic> beforeStatistics = SnapshotHelper.getSessionStatistics(c, SID);
            final Map<Long, OracleEvent> beforeEvents = SnapshotHelper.getSessionEvents(c, SID);

            // execute query remembering duration
            ResultSet originalResult = null;
            SQLException internalException = null;

            final long stime;
            mydbaTime += ((stime = System.nanoTime()) - mydbaSTime);
            try {
                originalResult = (ResultSet) thisJoinPoint.proceed();
            }
            catch (SQLException sqle) {
                internalException = sqle;
            }
            final long etime = System.nanoTime();
            mydbaSTime = etime;

            Execution e = null;
            try {
                if (internalException == null && connectionConfigOk) {
                    e = SnapshotHelper.insertStatementExecutionInformation(c,
                            SID,
                            query,
                            stime,
                            etime,
                            beforeStatistics,
                            beforeEvents,
                            -1);
                }
            }
            catch (SQLException ignored) {
                ignored.printStackTrace();
            }

            if (internalException != null) {
                throw internalException;
            }

            try {
                Map<String, Object> context;
                if ((context = contexts.get(Thread.currentThread())) == null) {
                    contexts.put(Thread.currentThread(), context = new HashMap<String, Object>());
                }

                Map<Object, Execution> executions = (Map<Object, Execution>) context.get(EXECUTIONS);

                if (executions == null) {
                    executions = new WeakHashMap<Object, Execution>(1);
                    context.put(EXECUTIONS, executions);
                }

                if (e != null) {
                    executions.put(originalResult, e);
                }
            }
            catch (Throwable t) {
                t.printStackTrace();
            }

            return originalResult;
        }
        finally {
            DBAHelper.close(c);
            mydbaTime += (System.nanoTime() - mydbaSTime);
            System.out.println("MyDBA time: " + (mydbaTime / 1000000.0) + " ms");
        }
    }

    /**
     * Pointcut to match int java.sql.Statement.executeUpdate(String).
     *
     * @param originalStmt the original statement on which the executeQuery has been called
     * @param query        the parameter of the call
     */
    @Pointcut("call(int java.sql.Statement.executeUpdate(String)) " +
            "&& !execution(int java.sql.Statement.executeUpdate(String)) " +
            "&& target(originalStmt) " +
            "&& args(query)")
    static void statementExecuteUpdatePointcut(final Statement originalStmt, final String query) {
    }

    /**
     * Around aspect that retrieves information regarding previous pointcut.
     *
     * @param thisJoinPoint join point for proceed processing
     * @param originalStmt  the original statement on which the executeUpdate has been called
     * @param query         the parameter of the call
     * @return an int as int java.sql.Statement.executeUpdate(String)
     * @throws Throwable (in fact an SQLException)
     */
    @Around("statementExecuteUpdatePointcut(originalStmt,query)")
    public int executeUpdate(final ProceedingJoinPoint thisJoinPoint,
                             final Statement originalStmt,
                             final String query) throws Throwable {

        final int SID = SnapshotHelper.getSessionSID(originalStmt.getConnection());

        Connection c = null;

        try {
            c = getConnection();

            final Map<Integer, OracleStatistic> beforeStatistics = SnapshotHelper.getSessionStatistics(c, SID);
            final Map<Long, OracleEvent> beforeEvents = SnapshotHelper.getSessionEvents(c, SID);

            // execute query remembering duration
            int rowCount = 0;
            SQLException internalException = null;

            final long stime = System.nanoTime();
            try {
                rowCount = (Integer) thisJoinPoint.proceed();
            }
            catch (SQLException sqle) {
                internalException = sqle;
            }
            final long etime = System.nanoTime();

            try {
                if (internalException == null && connectionConfigOk) {
                    SnapshotHelper.insertStatementExecutionInformation(c,
                            SID,
                            query,
                            stime,
                            etime,
                            beforeStatistics,
                            beforeEvents,
                            rowCount);
                }
            }
            catch (SQLException ignored) {
                ignored.printStackTrace();
            }

            if (internalException != null) {
                throw internalException;
            }

            return rowCount;
        }
        finally {
            DBAHelper.close(c);
        }
    }

    /**
     * Pointcut to match boolean java.sql.Statement.execute(String).
     *
     * @param originalStmt the original statement on which the executeQuery has been called
     * @param query        the parameter of the call
     */
    @Pointcut("call(boolean java.sql.Statement.execute(String)) " +
            "&& !execution(boolean java.sql.Statement.execute(String)) " +
            "&& target(originalStmt) " +
            "&& args(query)")
    static void statementExecutePointcut(final Statement originalStmt, final String query) {
    }

    /**
     * Around aspect that retrieves information regarding previous pointcut.
     *
     * @param thisJoinPoint join point for proceed processing
     * @param originalStmt  the original statement on which the executeQuery has been called
     * @param query         the parameter of the call
     * @return a boolean as boolean java.sql.Statement.execute(String)
     * @throws Throwable (in fact an SQLException)
     */
    @Around("statementExecutePointcut(originalStmt,query)")
    public boolean execute(final ProceedingJoinPoint thisJoinPoint,
                           final Statement originalStmt,
                           final String query) throws Throwable {
        Connection c = null;

        try {
            c = getConnection();

            final int SID = SnapshotHelper.getSessionSID(originalStmt.getConnection());

            final Map<Integer, OracleStatistic> beforeStatistics = SnapshotHelper.getSessionStatistics(c, SID);
            final Map<Long, OracleEvent> beforeEvents = SnapshotHelper.getSessionEvents(c, SID);

            // execute query remembering duration
            boolean originalResult = false;
            SQLException internalException = null;

            final long stime = System.nanoTime();
            try {
                originalResult = (Boolean) thisJoinPoint.proceed();
            }
            catch (SQLException sqle) {
                internalException = sqle;
            }
            final long etime = System.nanoTime();

            Execution e = null;
            if (originalResult) {
                try {
                    if (connectionConfigOk) {
                        e = SnapshotHelper.insertStatementExecutionInformation(c,
                                SID,
                                query,
                                stime,
                                etime,
                                beforeStatistics,
                                beforeEvents,
                                0);
                    }
                }
                catch (SQLException ignored) {
                    ignored.printStackTrace();
                }
            } else {
                try {
                    if (internalException == null && connectionConfigOk) {
                        e = SnapshotHelper.insertStatementExecutionInformation(c,
                                SID,
                                query,
                                stime,
                                etime,
                                beforeStatistics,
                                beforeEvents,
                                originalStmt.getUpdateCount());
                    }
                }
                catch (SQLException ignored) {
                    ignored.printStackTrace();
                }
            }

            if (internalException != null) {
                throw internalException;
            }

            if (originalResult) {
                try {
                    Map<String, Object> context;
                    if ((context = contexts.get(Thread.currentThread())) == null) {
                        contexts.put(Thread.currentThread(), context = new HashMap<String, Object>());
                    }

                    Map<Object, Execution> executions = (Map<Object, Execution>) context.get(EXECUTIONS);

                    if (executions == null) {
                        executions = new WeakHashMap<Object, Execution>(1);
                        context.put(EXECUTIONS, executions);
                    }

                    if (e != null) {
                        executions.put(originalStmt.getResultSet(), e);
                    }
                }
                catch (Throwable t) {
                    t.printStackTrace();
                }
            }

            return originalResult;
        }
        finally {
            DBAHelper.close(c);
        }
    }

    /**
     * Pointcut to match boolea java.sql.ResultSet.next().
     *
     * @param originalRset the original result set on which the next has been called
     */
    @Pointcut("call(boolean java.sql.ResultSet.next()) " +
            "&& !execution(boolean java.sql.ResultSet.next()) " +
            "&& target(originalRset)")
    static void nextPointcut(final ResultSet originalRset) {
    }

    /**
     * Around aspect that retrieves information regarding previous pointcut.
     *
     * @param thisJoinPoint join point for proceed processing
     * @param originalRset  the original result set on which the next has been called
     * @return a boolean as boolean java.sql.ResultSet.next()
     * @throws Throwable (in fact an SQLException)
     */
    @Around("nextPointcut(originalRset)")
    public boolean next(final ProceedingJoinPoint thisJoinPoint, final ResultSet originalRset)
            throws Throwable {

        // execute query remembering duration
        boolean originalResult = false;
        SQLException internalException = null;

        final long stime = System.nanoTime();
        try {
            originalResult = (Boolean) thisJoinPoint.proceed();
        }
        catch (SQLException sqle) {
            internalException = sqle;
        }

        if (internalException != null) {
            freeExecutionContext(originalRset);
            throw internalException;
        } else {
            final long etime = System.nanoTime();

            final Map<String, Object> context = contexts.get(Thread.currentThread());
            if (context != null) {

                final Map<Object, Execution> executions = (Map<Object, Execution>) context.get(EXECUTIONS);

                if (executions != null) {
                    final Execution e = executions.get(originalRset);

                    if (e != null) {

                        // again rows to process
                        final ResultSetStatistic rs = e.getResultSetStatistic();
                        if (rs == null) {
                            e.setResultSetStatistic(new ResultSetStatistic(stime, etime - stime));
                        } else {
                            rs.update(etime - stime);
                        }

                        SnapshotHelper.insertResultSetNextInformation(e);

                        // insert Oracle session statistics
                        Connection c = null;

                        try {
                            c = getConnection();

                            SnapshotHelper.insertExecutionStatistics(c, e);
                            SnapshotHelper.insertExecutionEvents(c, e);
                        }
                        finally {
                            DBAHelper.close(c);
                        }

                        if (!originalResult) {
                            freeExecutionContext(originalRset);
                        }
                    }
                }
            }
        }

        return originalResult;
    }

    /**
     * Store information of a statement preparation inside thread context.
     *
     * @param query                     the query
     * @param originalPreparedStatement the original PreparedStatement
     * @throws SQLException in case of problem
     */
    @AfterReturning(pointcut = "call(java.sql.PreparedStatement java.sql.Connection.prepareStatement(String)) " +
            "&& !execution(java.sql.PreparedStatement java.sql.Connection.prepareStatement(String)) " +
            "&& args(query)",
            returning = "originalPreparedStatement")
    public void connectionPrepareStatement(final String query, final PreparedStatement originalPreparedStatement)
            throws SQLException {

        Connection c = null;

        try {
            c = getConnection();

            final int SID = SnapshotHelper.getSessionSID(originalPreparedStatement.getConnection());

            final Map<Integer, OracleStatistic> beforeStatistics = SnapshotHelper.getSessionStatistics(c, SID);
            final Map<Long, OracleEvent> beforeEvents = SnapshotHelper.getSessionEvents(c, SID);

            final Execution e = new Execution(SID, query, beforeStatistics, beforeEvents);

            //System.out.println("Prep State: " + query);

            try {
                Map<String, Object> context;
                if ((context = contexts.get(Thread.currentThread())) == null) {
                    contexts.put(Thread.currentThread(), context = new HashMap<String, Object>());
                }

                Map<Object, Execution> executions = (Map<Object, Execution>) context.get(EXECUTIONS);

                if (executions == null) {
                    executions = new WeakHashMap<Object, Execution>(1);
                    context.put(EXECUTIONS, executions);
                }

                executions.put(originalPreparedStatement, e);
            }
            catch (Throwable t) {
                t.printStackTrace();
            }
        }
        finally {
            DBAHelper.close(c);
        }
    }

    /**
     * Pointcut to match java.sql.ResultSet java.sql.PreparedStatement.executeQuery().
     *
     * @param originalPreparedStatement the original prepared statement on which the executeQuery has been called
     */
    @Pointcut("call(java.sql.ResultSet java.sql.PreparedStatement.executeQuery()) " +
            "&& !execution(java.sql.ResultSet java.sql.PreparedStatement.executeQuery()) " +
            "&& target(originalPreparedStatement)")
    static void preparedStatementExecuteQueryPointcut(final PreparedStatement originalPreparedStatement) {
    }

    /**
     * Around aspect that retrieves information regarding previous pointcut.
     *
     * @param thisJoinPoint             join point for proceed processing
     * @param originalPreparedStatement the original prepared statement on which the executeQuery has been called
     * @return a ResultSet as java.sql.ResultSet java.sql.PreparedStatement.executeQuery()
     * @throws Throwable (in fact an SQLException)
     */
    @Around("preparedStatementExecuteQueryPointcut(originalPreparedStatement)")
    public ResultSet executeQuery(final ProceedingJoinPoint thisJoinPoint,
                                  final PreparedStatement originalPreparedStatement) throws Throwable {

        // retrieve previous execution information
        final Map<String, Object> context = contexts.get(Thread.currentThread());
        final Map<Object, Execution> executions = (Map<Object, Execution>) context.get(EXECUTIONS);
        final Execution e = executions.get(originalPreparedStatement);

        // execute query remembering duration
        ResultSet originalResult = null;
        SQLException internalException = null;

        final long stime = System.nanoTime();
        try {
            originalResult = (ResultSet) thisJoinPoint.proceed();
        }
        catch (SQLException sqle) {
            internalException = sqle;
        }
        final long etime = System.nanoTime();

        try {
            if (internalException == null && connectionConfigOk) {
                Connection c = null;

                try {
                    c = getConnection();

                    SnapshotHelper.insertPreparedStatementExecutionInformation(c,
                            e,
                            stime,
                            etime,
                            -1);
                }
                finally {
                    DBAHelper.close(c);
                }
            }
        }
        catch (SQLException ignored) {
            ignored.printStackTrace();
        }

        if (internalException != null) {
            throw internalException;
        }

        try {
            executions.put(originalResult, e);
        }
        catch (Throwable t) {
            t.printStackTrace();
        }

        return originalResult;
    }

    /**
     * Pointcut to match int java.sql.PreparedStatement.executeUpdate().
     *
     * @param originalPreparedStmt the original statement on which the executeQuery has been called
     */
    @Pointcut("call(int java.sql.PreparedStatement.executeUpdate()) " +
            "&& !execution(int java.sql.PreparedStatement.executeUpdate()) " +
            "&& target(originalPreparedStmt)")
    static void preparedStatementExecuteUpdatePointcut(final PreparedStatement originalPreparedStmt) {
    }

    /**
     * Around aspect that retrieves information regarding previous pointcut.
     *
     * @param thisJoinPoint        join point for proceed processing
     * @param originalPreparedStmt the original prepared statement on which the executeUpdate has been called
     * @return an int as int java.sql.PreparedStatement.executeUpdate()
     * @throws Throwable (in fact an SQLException)
     */
    @Around("preparedStatementExecuteUpdatePointcut(originalPreparedStmt)")
    public int executeUpdate(final ProceedingJoinPoint thisJoinPoint,
                             final Statement originalPreparedStmt) throws Throwable {

        // execute query remembering duration
        int rowCount = 0;
        SQLException internalException = null;

        final long stime = System.nanoTime();
        try {
            rowCount = (Integer) thisJoinPoint.proceed();
        }
        catch (SQLException sqle) {
            internalException = sqle;
        }
        final long etime = System.nanoTime();

        if (internalException != null) {
            freeExecutionContext(originalPreparedStmt);
            throw internalException;
        } else {
            final Map<String, Object> context = contexts.get(Thread.currentThread());
            if (context != null) {

                final Map<Object, Execution> executions = (Map<Object, Execution>) context.get(EXECUTIONS);

                if (executions != null) {
                    final Execution e = executions.get(originalPreparedStmt);

                    if (e != null) {

                        Connection c = null;

                        try {
                            c = getConnection();

                            // again rows to process
                            final UpdateStatistic us = e.getUpdateStatistic();
                            if (us == null) {
                                e.setUpdateStatistic(new UpdateStatistic(stime, rowCount, etime - stime));

                                System.out.println("Update: " + (etime - stime) + " / " + rowCount + " => insert prep information!");

                                SnapshotHelper.insertPreparedStatementExecutionInformation(c,
                                        e,
                                        stime,
                                        etime,
                                        rowCount);
                            } else {
                                us.update(rowCount, etime - stime);
                            }
                        }
                        catch (SQLException ignored) {
                            ignored.printStackTrace();
                        }
                        finally {
                            DBAHelper.close(c);
                        }
                    }
                }
            }
        }

        return rowCount;
    }

    /**
     * Store information of a prepared statement update execution information from thread context.
     *
     * @param originalPreparedStatement the original PreparedStatement
     * @throws SQLException in case of problem
     */
    @AfterReturning(pointcut = "call(void java.sql.PreparedStatement.close()) " +
            "&& !execution(void java.sql.PreparedStatement.close()) " +
            "&& target(originalPreparedStatement)")
    public void prepareStatementClose(final PreparedStatement originalPreparedStatement)
            throws SQLException {

        Connection c = null;

        try {
            final Map<String, Object> context = contexts.get(Thread.currentThread());
            if (context != null) {

                final Map<Object, Execution> executions = (Map<Object, Execution>) context.get(EXECUTIONS);

                if (executions != null) {
                    final Execution e = executions.get(originalPreparedStatement);

                    if (e != null && e.getUpdateStatistic() != null) {
                        c = getConnection();
                        System.out.println("Finally close PreparedStatement");

                        SnapshotHelper.insertUpdateInformation(e);

                        SnapshotHelper.insertExecutionStatistics(c, e);
                        SnapshotHelper.insertExecutionEvents(c, e);
                    }
                }
            }
        }
        catch (SQLException ignored) {
            ignored.printStackTrace();
        }
        finally {
            DBAHelper.close(c);
        }
    }

    /**
     * Store information of a prepared statement update execution information from thread context.
     *
     * @param originalStatement the original Statement (that could be a PreparedStatement)
     * @throws SQLException in case of problem
     */
    @AfterReturning(pointcut = "call(void java.sql.Statement.close()) " +
            "&& !execution(void java.sql.Statement.close()) " +
            "&& target(originalStatement)")
    public void statementClose(final Statement originalStatement)
            throws SQLException {

        Connection c = null;

        try {
            final Map<String, Object> context = contexts.get(Thread.currentThread());
            if (context != null) {

                final Map<Object, Execution> executions = (Map<Object, Execution>) context.get(EXECUTIONS);

                if (executions != null) {
                    final Execution e = executions.get(originalStatement);

                    if (e != null && e.getUpdateStatistic() != null) {
                        c = getConnection();

                        System.out.println("Finally close Statement");

                        SnapshotHelper.insertUpdateInformation(e);

                        SnapshotHelper.insertExecutionStatistics(c, e);
                        SnapshotHelper.insertExecutionEvents(c, e);
                    }
                }
            }
        }
        catch (SQLException ignored) {
            ignored.printStackTrace();
        }
        finally {
            DBAHelper.close(c);
        }
    }

    /**
     * Free execution context property linked to a thread.
     *
     * @param o the key
     */
    private void freeExecutionContext(final Object o) {
        ((Map<Object, Execution>) contexts.get(Thread.currentThread()).get(EXECUTIONS)).put(o, null);
    }
}
